package com.cxria.shiro.repository.impl;

import com.cxria.shiro.dto.ShiroStatus;
import com.cxria.shiro.enums.ShiroStatusCodeEnum;
import com.cxria.shiro.enums.ShiroStatusMsgEnum;
import com.cxria.shiro.exception.ShiroException;
import com.cxria.shiro.repository.BaseRepository;
import com.cxria.shiro.utils.Only;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;

import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

public class BaseRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
		implements BaseRepository<T, ID> {

	private final EntityManager em;

	private final Class<T> clazz;

	private ObjectMapper objectMapper = new ObjectMapper();

	public BaseRepositoryImpl(Class<T> clazz, EntityManager entityManager) {
		super(clazz, entityManager);
		this.clazz = clazz;
		this.em = entityManager;
	}

	@Override
	public <S extends T> S save(S entity) {
		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<T> cq = builder.createQuery(clazz);
		Root<T> root = cq.from(clazz);
		Field[] declaredFields = clazz.getDeclaredFields();
		List<Predicate> predicateList = new ArrayList<>();
		List<String> props = new ArrayList<>();
		Long id = null;
		S ent = null;
		try {
			for(Field field : declaredFields){
				String fieldName = field.getName();
				String methodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
				Method method = clazz.getDeclaredMethod(methodName);
				Object value = method.invoke(entity);
				if(methodName.equals("getId") && value != null){
					id = (Long) value;
				}
				boolean isAnnotationPresent = field.isAnnotationPresent(Only.class);
				if(isAnnotationPresent){
					Only only = field.getAnnotation(Only.class);
					if(only.isOnly()){
						props.add(fieldName);
						Predicate wh = builder.equal(root.get(fieldName).as(field.getType()), value);
						predicateList.add(wh);
					}
				}
			}
			Predicate delete = builder.equal(root.get("remove").as(Boolean.class), false);
			Predicate[] predicates = predicateList.toArray(new Predicate[predicateList.size()]);
			Predicate or = builder.or(predicates);
			Predicate[] and;
			if(id == null){
				and = new Predicate[]{or, delete};
			} else {
				Predicate notId = builder.notEqual(root.get("id").as(Long.class), id);
				and = new Predicate[]{notId, or, delete};
			}
			cq.where(and);
			TypedQuery<T> query = em.createQuery(cq);
			List<T> entitys = query.getResultList();
			int entitySize = entitys.size();
			if(entitySize > 0){
				StringBuilder errorMsg = new StringBuilder(ShiroStatusMsgEnum.DATA_ALREADY_EXISTS_ERROR + ", check attribute[");
				for(String prop : props){
					errorMsg.append(prop + ",");
				}
				errorMsg.append("]");
				errorMsg.deleteCharAt(errorMsg.length()-2);
				ShiroStatus status = new ShiroStatus(ShiroStatus.ERROR, ShiroStatusCodeEnum.DATA_ALREADY_EXISTS_ERROR, errorMsg.toString());
				String msg = objectMapper.writeValueAsString(status);
				throw new ShiroException(msg);
			}
			if(id != null){
				T one = findOne((ID) id);
				if(one != null){
					ent = super.save(entity);
				} else {
					String errorMsg = ShiroStatusMsgEnum.DATA_SAVE_ERROR + " : the object with id " + id + " does not exists!";
					ShiroStatus status = new ShiroStatus(ShiroStatus.ERROR, ShiroStatusCodeEnum.DATA_SAVE_ERROR, errorMsg.toString());
					String msg = objectMapper.writeValueAsString(status);
					throw new ShiroException(msg);
				}
			} else {
				ent = super.save(entity);
			}
		} catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException | JsonProcessingException e) {
			throw new RuntimeException(e);
		}
		return ent;
	}

	@Override
	public void delete(ID id) {
		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaUpdate<T> update = builder.createCriteriaUpdate(clazz);
		Root<T> root = update.from(clazz);
		update.where(builder.equal(root.get("id"), id));
		update.set(root.get("remove"), true);
		em.createQuery(update).executeUpdate();
	}

	@Override
	public T findOne(ID id) {
		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<T> cq = builder.createQuery(clazz);
		Root<T> root = cq.from(clazz);
		Predicate wh = builder.equal(root.get("id").as(Long.class), id);
		Predicate wh2 = builder.equal(root.get("remove").as(Boolean.class), false);
		Predicate[] predicates = {wh, wh2};
		cq.where(predicates);
		TypedQuery<T> query = em.createQuery(cq);
		List<T> entitys = query.getResultList();
		T entity = null;
		if(entitys.size() == 1){
			entity = entitys.get(0);
		}
		return entity;
	}

	@Override
	public List<T> findAll() {
		Specification<T> example = new Specification<T>() {
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
				Predicate remove = cb.equal(root.get("remove"), false);
				return cb.and(remove);
			}
		};
		Sort sort = new Sort(Sort.Direction.DESC, "id");
		return super.findAll(example, sort);
	}

	@Override
	public Page<T> findAll(Pageable pageable) {
		Specification<T> example = new Specification<T>() {
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
				Predicate remove = cb.equal(root.get("remove"), false);
				return cb.and(remove);
			}
		};
		Sort sort = pageable.getSort();
		if(sort == null){
			sort = new Sort(Sort.Direction.DESC, "id");
			pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), sort);
		}
		return super.findAll(example, pageable);
	}

	@Override
	public Page<T> findAll(Specification<T> spec, Pageable pageable) {
		Sort sort = pageable.getSort();
		if(sort == null){
			sort = new Sort(Sort.Direction.DESC, "id");
			pageable = new PageRequest(pageable.getPageNumber(), pageable.getPageSize(), sort);
		}
		return super.findAll(spec, pageable);
	}

}
